Sign In with Apple 后端开发完全指南
本周新做一个需求,为IOS APP接入苹果第三方登录。查看官方文档,发现其在IOS端操作描述是非常细致的,但在用户服务端的讲解实在是不知所云,让人头大。只能借助广大网友的智慧。本文并非完全原创,因为无论概念解读,还是操作方式,都是从各个文章处抄来的。本文最大的作用,在于针对自己和团队内部的开发记录,防止多次踩坑。
Sign in with Apple,对IOS和其它平台的处理方式是有很大差别的。本文只针对IOS平台,其它平台可以参考这篇文章,说得非常详细
说明
本文主要以官方文档为主线,辅以自己的理解。依赖于Sign in with Apple REST API页面,其分为两部分
用户授权和验证 - Authentication and Verification of Users
获取公钥和生成&验证token的API介绍
IOS上的苹果登录与一般的第三方登录最大的区别,在于IOS在客户端已经获取了必要的用户信息,以加密的形式发送给服务端,服务端需要做的验证并应用即可;而一般的第三方登录流程是需要在服务端请求用户信息的。秉持这一基础认识很重要,否则会云里雾里。
客户端
该部分对应 Authenticating Users with Sign in with Apple
客户端授权流程如上主要有如下几步
- 调用API发起授权请求
- 设备弹出授权框,请求用户授权
- 用户授权成功,API调用Apple ID 服务,请求用户信息
- 请求成功,Apple ID服务以返回三个字段:identity token、authorization code、user identifier
identity token
identity token是一个JWT,使用解析工具解开后如下,包含了基本的用户信息。
1 | { |
每个字段解读如下
头部:
kid: key id,在token验证时用于选取公钥的ID
alg: algorithm,算法
载荷部分:
- iss: isser的缩写,即JWT发布人
- aud: audience的缩写,客户,对应APP的开发者。对应Apple开发者账户中的client_id
- exp: expire的缩写,即过期时间
- iat: issue at的缩写,即该token的发布时间
- sub: subject,即加密对象,这是关键,它是用户的唯一标识符,很重要。
- nonce: 即随机字符串。用于绑定客户端会话和token的字符串值,用于防止重复攻击。
- nonce_supported: 指示对待nonce的方式
- true: 如果授权请求时有给nonce,但返回的token不含nonce,则说明此次请求失败
- false: 不支持nonce,忽略nonce
- c_hash: authorizationCode的hash值,用于验证authorizationCode
- email: 用户邮箱
- email_verified: 邮箱是否经过验证,总是为true
- is_private_email: 是否是加密邮箱,即上面说的[email protected]
- auth_time: 请求授权的时间
authorization code
authorization code用于和Apple ID服务交互,这里暂时用不到,忽略。
user identifier
JWT中的sub字段,对应了用户唯一标识符,即identifier,它具有如下特性
- 唯一且稳定
- 同一个苹果开发账户的所有APP对应的同一个用户的identifier是唯一的
- 不同的开发账户的APP对应一个用户的不同identifier
- 对于用户从APP注销,再登录,identifier是不会变的
- 可以用于唯一标示用户,即应该用它而不是邮箱嵌入我们的业务数据库
Private email
授权时,用户可以选择隐藏真是邮箱,于是我们会获取到一个[email protected]格式的用户邮箱,该邮箱具有一定限制
- 邮箱具有全局唯一性
- 发往该邮箱的信息将会被转发到真实的用户邮箱
- 对于同一个开发者的所有APP,一个用户对应一个邮箱;对于不同开发这的不同APP,一个用户对应多个邮箱
- 该邮箱一旦生成,会一直生效,无论用户是否有登录你的APP,或已经删除APP
- 要想向该邮箱发送信息,需要在Apple注册发送邮箱的邮箱域名,否则不会发送成功。
客户端最后一步
为了将用户与我方APP服务端用户系统绑定,需要将上面获取的的identity token、authorization code、user identifier等信息发送APP服务端。
服务端
这部分对应 Verifying a User,也是最令人头大的一部分。相同地,他也提供了一个流程图,描述如何使用authorization code换取用户信息和refresh token。对IOS登录的后台验证毫无帮助,相反会起到混淆视听的作用。请直接忽略。
对于IOS登录,由于IOS客户端已经通过客户端API获取了必要的用户信息:唯一标识符、name、email等,我们已经没有必要再次获取这些信息,只需要验证他们的真实性即可。
而对于其它平台的登录,如果你觉得看不懂这里的文档,强烈建议你按照这篇文章的方式操作——Sign in with Apple Tutorial, Part 4: Web and Other Platforms
验证identity token
identity token的签名是Apple ID服务使用私钥加密的,需要从这里获取公钥解密验证。取得的公钥以JWKS的形式呈现,如下。
1 | { |
公钥不止一个,需要取kid字段与JWT头部的kid字段匹配的那个。
这里我们使用了使用人数较多的库java-jwt、jwks-rsa-java。
1 | val jwt = JWT.decode(/*客户端上传的JWT*/) |
验证其它内容
根据官方手册,总计需要验证如下内容
- 使用公钥验证JWT签名,上一步已验证
- 验证nonce,可选,这里不验证
- 验证iss,必须为apple签发,即必须包含https://appleid.apple.com字符串
- 验证aud,必须为开发者账户的client_id
- 验证exp,即过期时间
使用现成库的好处之一是可以直接获取JWT的标准字段,如下
1 | Try { |
优化 - 缓存Algorithm实例
上述验证步骤中,在获取公钥和构建Algorithm实例时耗费较长时间——超过1秒
1 | val jwk = new UrlJwkProvider("https://appleid.apple.com/auth/keys").get(jwt.getKeyId) |
为了加快响应速度,可以缓存Algorithm实例,但由于apple提供的公钥可能变化,因此需要使用一定的策略兼顾效率和正确性。
实际操作如下
1 | def verifySignature(force: Boolean = false) = { |
总结
相对于传统第三方登录,IOS的登录流程略有不同
一般流程
客户端
- 用户被导入登录服务提供商,如微信、支付宝等
- 用户扫码或输入账号密码授权
- 服务提供商将用户重定向回客户端,并附带token
- 客户端凭借该token进行登录
服务端
- 应用服务端凭借该token向服务提供商验证登录真实性并获取用户信息
- 使用获取到的用户信息绑定自建的用户系统
IOS登录流程
客户端
- IOS客户端上弹出授权框
- 用户刷脸授权
- Apple同样将用户重定向回应用,附带token,但同时使用客户端API想Apple Server获取用户信息,以JWT形式提供
- 客户端将JWT发送给应用服务端
服务端
- 验证JWT
- 使用JWT附带的用户信息绑定自建的用户系统
可以看到,其最主要的不同还是在于IOS客户端已经获取了用户信息,在服务端仅需要验证JWT即可,与一般流程相比,IOS登录的应用后端少了一步请求用户信息的步骤。
没涉及的部分
文章到这里也只介绍了在IOS客户端接入Sign in with apple的后端操作步骤,并没有设计到IOS之外平台的处理方式,对此,可以参考这篇文章,它有很好的讲解。
更多
关于更多授权相关知识,这里列举了一些学习资源